/*
 * Copyright (c) 2007 jMouse Gestures
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *
 * * Neither the name of 'jMouse Gestures' nor the names of its contributors 
 *   may be used to endorse or promote products derived from this software 
 *   without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package com.jmg.swing;

import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Component;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Point;
import java.awt.Stroke;
import java.awt.Transparency;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import javax.swing.SwingUtilities;
import com.jmg.gesture.GestureManager;
import com.jmg.space.Location;
import com.jmg.gesture.event.GestureListener;
import com.jmg.util.IntArrayList;


/**
 * GestureTrailDrawer class.
 *
 * @author  Christopher Martin
 * @version $Id: GestureTrailDrawer.java 13 2007-05-24 12:13:13Z GentlemanHal $
 */
public class GestureTrailDrawer implements 
    GestureListener,
    ComponentListener
{
    /**
     * Default colour used if the Component argument only constructor is used to
     * create an instance. It is a slightly transparent light blue colour,
     * more formally it is:
     * <p>
     * <code>RGBA = 192, 192, 255, 200</code>.
     * </p>
     */
    public static final Color DEFAULT_COLOUR = new Color(192, 192, 255, 200);
    
    /**
     * Basic stroke which is rendered as a solid line 5pxs in width.  This is
     * used if the Component argument only constructor is used to create an
     * instance.
     */
    public static final Stroke BASIC_STROKE = new BasicStroke(
                            5f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL);
    
    /**
     * Default fader used if the Component only argument constructor is used to
     * create an instance.  This fader fades from the
     * {@link #DEFAULT_COLOUR default colour}.
     */
    public static final TrailFader DEFAULT_FADER = new TrailFader(
                                            DEFAULT_COLOUR, DEFAULT_COLOUR, 1f);
    
    /** Default number of updates to wait until repainting the trail.*/
    public static final int DEFAULT_PAINT_AFTER_UPDATES = 3;
    
    /** Colour to clear the buffer with. */
    private static final Color CLEAR_COLOUR = new Color(0, 0, 0, 0);
    
    private Graphics2D graphics;
    private Image backBuffer;
    private Component renderOn;
    private Color colour;
    private Stroke stroke;
    private int paintAfterUpdates, currentUpdates;
    private Point topLeft, bottomRight;
    private TrailFader fader;
    private TrailFadeTimer fadeTimer;
    private boolean inGesture, doingFadeEffect;
    private IntArrayList xPoints, yPoints;
    private int lastPaintedPointIndex;
    
    
    
    

    /**
     * Creates a new instance of <code>GestureTrailDrawer</code>.
     *
     * @param  renderOn The component to render the trail on.
     * @throws java.lang.IllegalArgumentException If a <code>null</code> argument is
     *         passed.
     */
    public GestureTrailDrawer(Component renderOn) 
        throws IllegalArgumentException
    {
        this(renderOn, DEFAULT_COLOUR, BASIC_STROKE, DEFAULT_FADER);
    }
    
    /**
     * Creates a new instance of <code>GestureTrailDrawer</code>.
     * 
     * @param  renderOn The component to render the trail on.
     * @param  colour The colour of the trail.
     * @param  stroke The stroke used to render the trail.
     * @param  fader The fader used to fade finished trails.
     * @throws java.lang.IllegalArgumentException If a <code>null</code> argument is
     *         passed.
     */
    public GestureTrailDrawer(Component renderOn, Color colour, 
                              Stroke stroke, TrailFader fader) 
        throws IllegalArgumentException
    {
        if (null == renderOn) {
            throw new IllegalArgumentException("renderOn was null!");
        }
        if (null == colour) {
            throw new IllegalArgumentException("colour was null");
        }
        if (null == stroke) {
            throw new IllegalArgumentException("stroke was null");
        }
        if (null == fader) {
            throw new IllegalArgumentException("fader was null");
        }
        
        
        this.renderOn = renderOn;
        this.colour   = colour;
        this.stroke   = stroke;
        this.fader    = fader;
        
        renderOn.addComponentListener(this);
        initVariables();
    }
    
    /** Initialises the instance variables. */
    private void initVariables() {
        paintAfterUpdates     = DEFAULT_PAINT_AFTER_UPDATES;
        currentUpdates        = 0;
        topLeft               = new Point( Integer.MAX_VALUE,  Integer.MAX_VALUE);
        bottomRight           = new Point(-Integer.MAX_VALUE, -Integer.MAX_VALUE);
        fadeTimer             = new TrailFadeTimer(0.1f, this);
        inGesture             = false;
        doingFadeEffect       = false;
        xPoints               = new IntArrayList();
        yPoints               = new IntArrayList();
        lastPaintedPointIndex = 0;
    }

    
    
    
    
    /**
     * Returns the colour of the trail.
     * 
     * @return The trails colour.
     * @see    #setColour
     */
    public Color getColour() {
        return colour;
    }
    
    /**
     * Sets the colour of the trail.  If the colour is changed it is
     * recommended that the <code>TrailFaders</code> start colour is also
     * changed so that they match.
     * 
     * @param  colour The trails new colour.
     * @throws java.lang.IllegalArgumentException If a <code>null</code> argument is
     *         passed.
     * @see    #getColour
     * @see    #setFader
     */
    public void setColour(final Color colour)
        throws IllegalArgumentException
    {
        if (null == colour) {
            throw new IllegalArgumentException("colour was null!");
        }
        
        
        this.colour = colour;
    }
    
    /**
     * Returns the stroke used to render the trail.
     * 
     * @return The trails stroke.
     * @see    #setStroke
     */
    public Stroke getStroke() {
        return stroke;
    }
    
    /**
     * Sets the stroke used to render the trail.
     * 
     * @param  stroke The trails new stroke.
     * @throws java.lang.IllegalArgumentException If a <code>null</code> argument is
     *         passed.
     * @see    #getStroke
     */
    public void setStroke(final Stroke stroke) 
        throws IllegalArgumentException
    {
        if (null == stroke) {
            throw new IllegalArgumentException("stroke was null!");
        }
            
            
        this.stroke = stroke;
    }
    
    /**
     * Sets the number of gesture updates this drawer receives before it redraws
     * the trail.  The lower the number the more often the trail is redrawn and
     * the higher the number the less often it's redrawn.
     * 
     * @param  pau The number of updates to receive before repainting.
     * @throws java.lang.IllegalArgumentException If pau is less than or equal
     *         to zero.
     * @see    #getPaintAfterUpdates
     */
    public void setPaintAfterUpdates(final int pau) 
        throws IllegalArgumentException
    {
        if (pau <= 0) {
            throw new IllegalArgumentException("pau must be > 0! " + pau);
        }
        
        
        paintAfterUpdates = pau;
    }
    
    /**
     * Returns the number of gesture updates this drawer receives before
     * repainting the trail.
     * 
     * @return Updates to wait before repainting.
     * @see    #setPaintAfterUpdates
     */
    public int getPaintAfterUpdates() {
        return paintAfterUpdates;
    }
    
    /**
     * Sets the fader used by this drawer.
     * 
     * @param  fader The new fader.
     * @throws java.lang.IllegalArgumentException If a <code>null</code> argument is
     *         passed.
     * @see    #getFader
     */
    public void setFader(final TrailFader fader)
        throws IllegalArgumentException
    {
        if (null == fader) {
            throw new IllegalArgumentException("fader was null!");
        }
        
        
        this.fader = fader;
    }
    
    /**
     * Returns the fader used by this drawer.
     * 
     * @return The fader.
     * @see    #setFader
     */
    public TrailFader getFader() {
        return fader;
    }
    
    /**
     * Sets the trail fader used by this drawer.
     * 
     * @param  timer The timer to use.
     * @throws java.lang.IllegalArgumentException If a <code>null</code> argument is
     *         passed.
     * @see    #getTrailFadeTimer
     */
    public void setTrailFadeTimer(final TrailFadeTimer timer) 
        throws IllegalArgumentException
    {
        if (null == timer) {
            throw new IllegalArgumentException("timer was null!");
        }
        
        
        fadeTimer = timer;
    }
    
    /**
     * Gets the trail fader used by this drawer.
     * 
     * @return The trail fader.
     * @see    #setTrailFadeTimer
     */
    public TrailFadeTimer getTrailFadeTimer() {
        return fadeTimer;
    }
    
    /**
     * Returns the component being rendered on.
     * 
     * @return The component.
     */
    public Component getComponentBeingRenderedOn() {
        return renderOn;
    }
    
    /** Paints the trail. */
    public void paint() {
        if (inGesture) {
            graphics.setColor(colour);
            
        } else if (doingFadeEffect) {
            clear();
            lastPaintedPointIndex = 0;
            
            graphics.setColor(fader.getInterpolatedColour(System.currentTimeMillis()));

            if (fader.shouldReset()) {
                resetAll();
                return;
            }
            
        } else {
            return;
        }
        
        graphics.drawPolyline(xPoints.subList(lastPaintedPointIndex, xPoints.size()), 
                              yPoints.subList(lastPaintedPointIndex, yPoints.size()), 
                              xPoints.size() - lastPaintedPointIndex);
        
        lastPaintedPointIndex = xPoints.size() - 1;
        
        renderOn.getGraphics().drawImage(backBuffer, 0, 0, null);
    }
    
    /** Clears the back buffer. */
    public void clear() {
        Color oldColour = graphics.getColor();

        graphics.setColor(CLEAR_COLOUR);

        graphics.fillRect(topLeft.x, topLeft.y, 
                          bottomRight.x - topLeft.x, 
                          bottomRight.y - topLeft.y);

        graphics.setColor(oldColour);
    }
    
    /** Initialises the back buffer used for double buffering. */
    private void initBackBuffer() {
        backBuffer = renderOn.getGraphicsConfiguration().createCompatibleImage(
            renderOn.getWidth()  > 0 ? renderOn.getWidth()  : 1, 
            renderOn.getHeight() > 0 ? renderOn.getHeight() : 1,
            Transparency.TRANSLUCENT);

        destroyBackBuffer();
        
        graphics = (Graphics2D) backBuffer.getGraphics();
        
        graphics.setComposite(AlphaComposite.Src);
        graphics.setStroke(stroke);
    }
    
    /** Destroys the back buffer. */
    private void destroyBackBuffer() {
        if (graphics != null) {
            graphics.dispose();
            
            graphics = null;
        }
    }
    
    /**
     * Updates the clip region.
     * 
     * @param x The x value that needs to be inside the clip.
     * @param y The y value that needs to be inside the clip.
     */
    private void updateClipRegion(int x, int y) {
        int strokeWidth = (int) ((BasicStroke) stroke).getLineWidth();
        
        if (x >= bottomRight.x) {
            bottomRight.x = x + strokeWidth;
        }
        
        if (x <= topLeft.x) {
            topLeft.x = x - strokeWidth;
        }
        
        if (y >= bottomRight.y) {
            bottomRight.y = y + strokeWidth;
        }
        
        if (y <= topLeft.y) {
            topLeft.y = y - strokeWidth;
        }
    }
    
    /** Resets the clip region back to default. */
    private void resetClipRegion() {
        topLeft.x = Integer.MAX_VALUE;  
        topLeft.y = Integer.MAX_VALUE;
        bottomRight.x = -Integer.MAX_VALUE;
        bottomRight.y = -Integer.MAX_VALUE;
    }
    
    /** Resets the mouse points. */
    private void resetPoints() {
        xPoints.clear();
        yPoints.clear();
        
        lastPaintedPointIndex = 0;
    }
    
    /** Resets the fader. */
    private void resetFader() {
        fader.reset();
        doingFadeEffect = false;
        fadeTimer.stop();
    }
    
    /** Resets everything. */
    private void resetAll() {
        resetFader();
        clear();
        resetClipRegion();
        resetPoints();
    }
    
    /**
     * Converts the given point from the managers registered components
     * coordinate space to the renderOn components coordinate space.
     * 
     * @param point The point to convert.
     * @param manager The manager that holds the component.
     */
    private void convertPoint(Point point, GestureManager<?> manager) {
        if (manager.getRegisteredComponent() instanceof Component) {
            SwingUtilities.convertPointToScreen(
                           point, (Component) manager.getRegisteredComponent());
            
            SwingUtilities.convertPointFromScreen(point, renderOn);
        } 
    }
    
    /**
     * Called when a gesture is started.
     * <p>
     * <strong>This method should not be called directly.</strong>
     * </p>
     * 
     * @param when The time this event happened.
     * @param where The location this event happened.
     * @param manager The manager that detected this event.
     */
    public void gestureStarted(long when, Location where, GestureManager<?> manager) {
        resetAll();
        
        Point p = new Point((int) where.getX(), (int) where.getY());
        
        convertPoint(p, manager);
        
        xPoints.add(p.x);
        yPoints.add(p.y);
        
        updateClipRegion(p.x, p.y);
        
        inGesture = true;
    }
    
    /**
     * Called when the mouse is moved while in a gesture.
     * <p>
     * <strong>This method should not be called directly.</strong>
     * </p>
     * 
     * @param when The time this event happened.
     * @param where The location this event happened.
     * @param manager The manager that detected this event.
     */
    public void gestureMovement(long when, Location where, GestureManager<?> manager) {
        Point p = new Point((int) where.getX(), (int) where.getY());
        
        convertPoint(p, manager);
        
        xPoints.add(p.x);
        yPoints.add(p.y);
        
        updateClipRegion(p.x, p.y);
        
        if (++currentUpdates == paintAfterUpdates) {
            if (renderOn.isVisible()) {
                paint();
            }
            currentUpdates = 0;
        }
        
    }

    /**
     * Called when a gesture is finished.
     * <p>
     * <strong>This method should not be called directly.</strong>
     * </p>
     * 
     * @param when The time this event happened.
     * @param where The location this event happened.
     * @param manager The manager that detected this event.
     */
    public void gestureFinished(long when, Location where, GestureManager<?> manager) {
        Point p = new Point((int) where.getX(), (int) where.getY());
        
        convertPoint(p, manager);
        
        xPoints.add(p.x);
        yPoints.add(p.y);
        
        updateClipRegion(p.x, p.y);
        
        inGesture = false;
        doingFadeEffect = true;
        fadeTimer.start();
    }

    /**
     * Used to resize the back buffer.
     * <p>
     * <strong>This method should not be called directly.</strong>
     * </p>
     * 
     * @param e Not used.
     */
    public void componentResized(ComponentEvent e) {
        initBackBuffer();
    }

    /**
     * Initialises the back buffer.
     * <p>
     * <strong>This method should not be called directly.</strong>
     * </p>
     *
     * @param e Not used.
     */
    public void componentShown(ComponentEvent e)  {
        initBackBuffer();
    }
    
    /**
     * Destroys the back buffer.
     * <p>
     * <strong>This method should not be called directly.</strong>
     * </p>
     *
     * @param e Not used.
     */
    public void componentHidden(ComponentEvent e) {
        destroyBackBuffer();
    }
    
    /** This method is not used by this class. */
    public void componentMoved(ComponentEvent e) { /* NOOP */ }  

    
    
    
    
    /**
     * Returns a string representation of the object.
     * 
     * @return a string representation of the object.
     */
    @Override
    public String toString() {
        StringBuilder buf = new StringBuilder(30);
        
        buf.append(getClass().getName());
        buf.append("[colour=").append(colour); 
        buf.append(']');
        
        return buf.toString();
    }
    
}
